package ca.cbc.msb.mediacms;

import java.io.IOException;
import javax.jms.JMSException;
import javax.naming.NamingException;

import ca.cbc.msb.util.ReadAndWriteFileUtil;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ca.cbc.msb.exception.ThePlatformAuthenticationException;
import ca.cbc.msb.exception.ThePlatformNonBlockingException;
import ca.cbc.msb.exception.ThePlatformSequenceIdNotFoundException;
import ca.cbc.msb.util.AppConfig;
import ca.cbc.msb.util.HttpServiceUtil;

public class NotificationListener implements Runnable {
	private static Logger logger = LoggerFactory.getLogger(NotificationListener.class);
    //private static Logger progressLogger = LoggerFactory.getLogger("progress");
	private static String PL_USERNAME;
	private static String PL_PASSWORD;
	//How often does Listener monitoring URL 
	private static int SLEEP_TIMEOUT;
	//The JSON feed # per visit
	private static int FEED_SIZE_PER_FETCH;
	private static String AUTHENTICATION_URL;
	private static String NOTIFICATION_URL;
	
	
	//Default Token String for Authentication
	private String tokenStr;
	private NotificationHandler notificationHandler;
	private long sequenceId;
	private Thread listenerThread;
	private volatile boolean running;
    private ReadAndWriteFileUtil rwSequenceIdUtil;

	public NotificationListener(NotificationHandler nh)
	{
		PL_USERNAME = AppConfig.getProperty("thePlatform.username");
		PL_PASSWORD = AppConfig.getProperty("thePlatform.password");
		SLEEP_TIMEOUT = Integer.parseInt(AppConfig.getProperty("notification.check.interval"))*1000;
		FEED_SIZE_PER_FETCH = Integer.parseInt(AppConfig.getProperty("nl.feedsizeperfetch"));
		AUTHENTICATION_URL = AppConfig.getProperty("thePlatform.authentication.url");
		NOTIFICATION_URL = AppConfig.getProperty("thePlatform.notification.url");
         rwSequenceIdUtil= new ReadAndWriteFileUtil(nh.getSequenceFileName());

		this.notificationHandler = nh;
		try {
			this.setTokenStr(this.fetchCurrentToken(PL_USERNAME, PL_PASSWORD));
		} catch (Exception e) {
			logger.error("Exception when try to get token", e);
		}

         String tmpStr = rwSequenceIdUtil.readLine();
        if(tmpStr == null)
        	this.setSequenceId(0);
        else
            this.setSequenceId(Long.parseLong(tmpStr));

		if(this.getSequenceId() == 0)
		{
			logger.info("Sequence Id read from file failed, now using current sequence Id from thePlatform...");
			this.setSequenceId(this.fetchCurrentSequenceId());
			logger.debug("New sequenceId is {}...", this.getSequenceId());
		}
	}

    public void start(){
    	running = true;
    	listenerThread = new Thread(this);
    	listenerThread.start();
    }

    public void stop(){
		running=false;
		try {
			listenerThread.join(30000);
			listenerThread.interrupt();
		} catch (InterruptedException e) {
			logger.error("sleeping before stop consumer interrupted.", e);
		} finally {
             notificationHandler.cleanUp();
        }
    }
	
	@Override
	public void run() 
	{   
		boolean isGoToSleep = false;
		String jsonFeedStr = "";
        while(running)
        {
        	//listen to URL for JSON feed
        	if(!isGoToSleep)
        		jsonFeedStr = this.listen();
                updateManagementMBean("success");
        	
        	//no feed has found then goes to sleep for some time
        	if(jsonFeedStr == null || jsonFeedStr.equals("") || isGoToSleep || jsonFeedStr.equals("[]"))
        	{
        		//logger.debug("{} listener goes to sleep for {} seconds...", notificationHandler.getClass().getName(), SLEEP_TIMEOUT/1000.0);
        		isGoToSleep = false;
	        	try {
					Thread.sleep(SLEEP_TIMEOUT);
				} catch (InterruptedException e) {
					logger.error("InterruptedException", e);
				} catch (Exception e) {
					logger.error("!!!Unknown Error when listening the notification", e);
				}
        	}
        	else //process the feed
        	{    			
	        	long nextSequenceId = 0;
				try {
					NotificationDigest.OutputPair pair  = new NotificationDigest().digest(jsonFeedStr);
                    if(pair.notifications!=null){
					notificationHandler.handleMessage(pair.notifications);
                    nextSequenceId = pair.notifications.get(pair.notifications.size()-1).getId();
                    } else if (pair.id != 0){
                        //logger.debug("Empty notifications will use new sequence id {}", pair.id);
                        nextSequenceId = pair.id;
                    }
				} catch (NamingException e) {
					logger.error("Naming Exception", e);
				} catch (JMSException e) {
					logger.error("JSM Exception", e);
				} catch (ThePlatformAuthenticationException e) {
					logger.info("The platform authentication exception, try to update token...");
					try {
						this.setTokenStr(this.fetchCurrentToken(PL_USERNAME, PL_PASSWORD));
						logger.debug("New token is: {}", this.getTokenStr());
						//use previous sequenceId and listen again
						nextSequenceId = this.getSequenceId();
					} catch (IOException e1) {
						logger.error("Exception when try to get token", e);
					}
				} catch (ThePlatformNonBlockingException e) {
					logger.debug("No feed avaiable for fetching......");
					//use previous sequenceId and go to sleep
        			nextSequenceId = this.getSequenceId();
        			isGoToSleep = true;
				} catch (ThePlatformSequenceIdNotFoundException e) {
					logger.info("The platform sequence id not found exception, try to update sequence id...");
					this.setSequenceId(this.fetchCurrentSequenceId());
					logger.debug("New Sequence Id is: {}", this.getSequenceId());
					nextSequenceId = this.getSequenceId();
				} catch (Exception e) {
					logger.error("!!!Unknow Error when digesting the notification", e);
				}
	        	this.setSequenceId(nextSequenceId);
                rwSequenceIdUtil.writeLine(String.valueOf(nextSequenceId));
        	}
        }
	}

    private void updateManagementMBean(String message) {
        //Date d = new Date();
        if (notificationHandler instanceof ChangedMediaNotificationHandler){
            if(message.equals("successful")){
//                MSBManagement.instance.setChangeMediaStatus(new MSBStatus(new Date(), new Date(), message));
            }else{
//                MSBManagement.instance.setChangeMediaStatus(new MSBStatus(new Date(), message));
            }
        } else if ( notificationHandler instanceof NewReleaseNotificationHandler){
             if(message.equals("successful")){
//                MSBManagement.instance.setNewMediaStatus(new MSBStatus(new Date(), new Date(), message));
            }else{
//                MSBManagement.instance.setNewMediaStatus(new MSBStatus(new Date(), message));
            }
        }else if ( notificationHandler instanceof  DeletedReleaseNotificationHandler){
            if(message.equals("successful")){
//                MSBManagement.instance.setDeleteMediaStatus(new MSBStatus(new Date(), new Date(), message));
            }else{
//                MSBManagement.instance.setDeleteMediaStatus(new MSBStatus(new Date(), message));
            }
        }
    }

    /**
	 * This methods return current sequence id
	 * @return sequence id long
	 */
	public long fetchCurrentSequenceId()
	{
		StringBuffer URLStrBuf = new StringBuffer(NOTIFICATION_URL);
		URLStrBuf.append("&" + notificationHandler.getFilterParam());
		long sequenceId = 0;
		try {
			sequenceId = parseSequenceId(HttpServiceUtil.postText(URLStrBuf.toString(), "token=" + this.getTokenStr()));
		} catch (IOException e) {
			logger.error("Error posting text...", e);
		}
		return sequenceId;
	}
	
	/**
	 * This methods return current token with user name and password input
	 * @param userName -- string
	 * @param passwd -- string
	 * @return token string
	 * @throws IOException 
	 */
	public String fetchCurrentToken(String userName, String passwd) throws IOException
	{			   
		String postStr = "username=" + userName + "&password=" + passwd;
		String tokenStr = null;
		
		tokenStr = parseToken(HttpServiceUtil.postText(AUTHENTICATION_URL, postStr));
		
		return tokenStr;
	}
	
	/**
	 * This methods takes some parameters and visit the Platform URL to get JSON feed
	 * @return JSON feed string
	 * The platform notify parameter table:
	 * https://help.theplatform.com/display/wsf2/Subscribing+to+change+notifications#Subscribingtochangenotifications-Filteringnotifications
	 */
	private String listen()
	{
		StringBuilder URLStrBuf = new StringBuilder(NOTIFICATION_URL);

        URLStrBuf.append("&").append(notificationHandler.getFilterParam());
		
		//make sure tokenStr is not empty
		if(tokenStr == null || tokenStr.trim().equals(""))
		{
			logger.error("Token can not be empty!");
			return null;			
		}
		
		if(this.sequenceId != 0)
            URLStrBuf.append("&since=").append(this.sequenceId);
		else
			logger.error("ERROR: Sequence Id is null.");
		
		if(FEED_SIZE_PER_FETCH > 0)
            URLStrBuf.append("&size=").append(FEED_SIZE_PER_FETCH);
		else
			logger.error("ERROR: Fetch size is less than zero.");
			
		String response = null;			
		try {
			response = HttpServiceUtil.postText(URLStrBuf.toString(), "token=" + tokenStr);
		} catch (IOException e) {
			logger.error("Error posting text...", e);
		} catch (Exception e) {
			logger.error("Exception in fetching the notification for" + URLStrBuf.toString(), e);
		}
		
		return response;
	}
	
	/**
	 * This methods takes a JSON string and parse sequence id from the string
	 * @param jsonStr - string
	 * @return sequence id string
	 */
	protected static long parseSequenceId(String jsonStr)
	{
		if(jsonStr == null || jsonStr.equals(""))
		{
			logger.warn("Digesting empty notification JSON text!");
			return 0;
		}
		return JSONArray.fromObject(jsonStr).getJSONObject(0).getLong("id");	 
	}
	
	/**
	 * This methods takes a JSON string and parse token string from the JSON text
	 * @param jsonStr - token string
	 * @return token string
	 */
	protected static String parseToken(String jsonStr)
	{
		if(jsonStr == null || jsonStr.equals(""))
		{
			logger.warn("Digesting empty notification JSON text!");
			return null;
		}
		
		JSONObject jsonObject = (JSONObject) JSONSerializer.toJSON(jsonStr);
		return jsonObject.getJSONObject("signInResponse").getString("token");	 
	}

	/*
	 * Getter and Setter
	 */

	public long getSequenceId() {
		return sequenceId;
	}

	public String getTokenStr() {
		return tokenStr;
	}

	public void setTokenStr(String tokenStr) {
		this.tokenStr = tokenStr;
	}

	public void setSequenceId(long sequenceId) {
		this.sequenceId = sequenceId;
	}
}
